package com.nulldev.util.uri.yt;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import com.nulldev.util.encoding.base64.Base64;
import com.nulldev.util.error.ErrorCallback;
import com.nulldev.util.io.FileUtil;
import com.nulldev.util.java.DataSet;
import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.scripting.nScripting;
import com.nulldev.util.lang.json.Json;
import com.nulldev.util.lang.json.JsonArray;
import com.nulldev.util.lang.json.JsonObject;
import com.nulldev.util.lang.json.JsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.math.TimeUtil;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.CachedItem;
import com.nulldev.util.other.CharsetUtil;
import com.nulldev.util.promise.Promise;
import com.nulldev.util.promise.Promiser;
import com.nulldev.util.string.StringsUtil;
import com.nulldev.util.uri.URIUtil;
import com.nulldev.util.uri.URIUtil2;
import com.nulldev.util.uri.Soup_imported.Jsoup;
import com.nulldev.util.uri.Soup_imported.nodes.Document;
import com.nulldev.util.uri.Soup_imported.nodes.Element;
import com.nulldev.util.uri.Soup_imported.select.Elements;
import com.nulldev.util.uri.yt.Format.ACodec;
import com.nulldev.util.uri.yt.Format.VCodec;
import com.nulldev.util.uri.yt.YTSig.YTSigSoftwareDecoder;
import com.nulldev.util.uri.yt.YTSig.YTSignatureDecrypter;
import com.nulldev.util.uri.yt.comments.CommentMeta;
import com.nulldev.util.uri.yt.comments.YTComments;
import com.nulldev.util.uri.yt.webVTT.WebVTTTransform;

/**
 * WARNING: This class will soon be deprecated due to a rewrite being worked on.
 * @author null
 * @since nullUtil-2.5
 * @supportedTill 2.7
 */
public abstract class YouTubeExtractor {
	
	public static final float VERSION = 1.042f;
	public static final String VERSION_STRING = "1.0.4.2";
	
	private static final Logger logger = LoggerFactory.getLoggerD(true, true, true);
    private final static boolean CACHING = true;

    static boolean LOGGING = JVMUtil.isArg("--trace");

    private final static String LOG_TAG = "YouTubeExtractor";
    private final static String CACHE_FILE_NAME = "decipher_js_funct";
    private final static int DASH_PARSE_RETRIES = 5;

    private String videoID;
    private VideoMeta videoMeta;
    private boolean includeWebM = true;
    private boolean useHttp = false;
    private boolean parseDashManifest = false;

    private volatile String decipheredSignature;
    
    private static String prevVideo;
    private static CachedItem<YTSignatureDecrypter> decrypter = 
    		new CachedItem<YTSig.YTSignatureDecrypter>(TimeUtil.fromMinutes(30)) {
		
		@Override
		public YTSignatureDecrypter accquire() throws Exception {
			if (prevVideo == null) {
				prevVideo = "qzd8ZM2tl-8"; // Just a random one, will change every few releases.
				return this.accquire();
			}
			String pageMap = URIUtil.downloadPage(
					URIUtil.safeCreate("https://www.youtube.com/watch?v=" + prevVideo));
			String jsData = null;
			try {
				String ex = StringsUtil.substringBetweenO(pageMap, "<script src=\"/yts/jsbin/player-", "\"");
				boolean usingFix = false;
				if (ex == null && pageMap.contains("/yts/jsbin/player_ias")) {
					ex = StringsUtil.substringBetweenO(pageMap,
							"<script src=\"/yts/jsbin/player_", "\"");
					usingFix = true;
				}
				if (ex == null) {
					final String newPageInfo = URIUtil.downloadPage(URIUtil.safeCreate(
	    					"https://www.youtube.com/watch?v=" + prevVideo + "&bpctr=" + System.currentTimeMillis()));
					pageMap = newPageInfo;
	    			ex = StringsUtil.substringBetweenO(pageMap, "<script src=\"/yts/jsbin/player-", "\"");
				}
				if (ex == null) {
					logger.error("Unable to get player JS!");
					//if (this.logger.traceEnabled) {
					//	this.logger.trace("Printing HTML...");
					////	this.logger.debugC(pageMap);
					//}
					logger.info("*bzzzzzzzzzzzzzzzzzz* restart et");
					return this.accquire();
				}
				String url = "https://s.ytimg.com/yts/jsbin/player-" +
	    				ex;
				if (usingFix) {
					url = "https://s.ytimg.com/yts/jsbin/player_" +
		    				ex;
				}
				logger.debug("FastMode2: Player URL: " + url);
				jsData = URIUtil.downloadPage(
	    				URIUtil.safeCreate(url));
			}catch (IOException e) {
				throw new RuntimeException(e);
			}
			return YTSig.accquire(jsData);
		}
	};
	
	public static void __static_call() {}

    private static String decipherJsFileName;
    private static String decipherFunctions;
    private static String decipherFunctionName;

    private final Lock lock = new ReentrantLock();
    private final Condition jsExecuting = lock.newCondition();

    private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36";
    private static final String STREAM_MAP_STRING = "url_encoded_fmt_stream_map";

    private static final Pattern patYouTubePageLink = Pattern.compile("(http|https)://(www\\.|m.|)youtube\\.com/watch\\?v=(.+?)( |\\z|&)");
    private static final Pattern patYouTubeShortLink = Pattern.compile("(http|https)://(www\\.|)youtu.be/(.+?)( |\\z|&)");

    private static final Pattern patDashManifest1 = Pattern.compile("dashmpd=(.+?)(&|\\z)");
    private static final Pattern patDashManifest2 = Pattern.compile("\"dashmpd\":\"(.+?)\"");
    private static final Pattern patDashManifestEncSig = Pattern.compile("/s/([0-9A-F|.]{10,}?)(/|\\z)");

    private static final Pattern patTitle = Pattern.compile("title=(.*?)(&|\\z)");
    private static final Pattern patAuthor = Pattern.compile("author=(.+?)(&|\\z)");
    private static final Pattern patChannelId = Pattern.compile("ucid=(.+?)(&|\\z)");
    private static final Pattern patLength = Pattern.compile("length_seconds=(\\d+?)(&|\\z)");
    private static final Pattern patViewCount = Pattern.compile("view_count=(\\d+?)(&|\\z)");
    private static final Pattern patAvgRating = Pattern.compile("avg_rating=(.*?)(&|\\z)");

    private static final Pattern patHlsvp = Pattern.compile("hlsvp=(.+?)(&|\\z)");
    private static final Pattern patHlsItag = Pattern.compile("/itag/(\\d+?)/");

    private static final Pattern patItag = Pattern.compile("itag=([0-9]+?)([&,])");
    private static final Pattern patEncSig = Pattern.compile("s=([0-9A-F|.]{10,}?)([&,\"])");
    private static final Pattern patIsSigEnc = Pattern.compile("s%3D([0-9A-F|.]{10,}?)%26");
    private static final Pattern patUrl = Pattern.compile("url=(.+?)([&,])");
    private static final Pattern patTags = Pattern.compile("(?s)(?<=keywords=).*?(?=&)");

    private static final Pattern patVariableFunction = Pattern.compile("([{; =])([a-zA-Z$][a-zA-Z0-9$]{0,2})\\.([a-zA-Z$][a-zA-Z0-9$]{0,2})\\(");
    private static final Pattern patFunction = Pattern.compile("([{; =])([a-zA-Z$_][a-zA-Z0-9$]{0,2})\\(");
    private static final Pattern patDecryptionJsFile = Pattern.compile("jsbin\\\\/(player-(.+?).js)");
    
    @Deprecated
    /**
     * No longer used as of Sept. 2018 (YouTubeExtractor 1.0.4.1), switched to an adaptive signature decryption method.
     */
    private static final Pattern patSignatureDecFunction = 
    		/* Designed for pre Sept 2018, YouTube began obfuscating their code to be total assholes. */
    		Pattern.compile("\"signature\",(.{1,3}?)\\(.{1,10}?\\)");
    		//.compile("(\\w+)\\s*=\\s*function\\((\\w+)\\){\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;");

    private static final HashMap<Integer, Format> FORMAT_MAP = new HashMap<Integer, Format>();

    static { 
        // http://en.wikipedia.org/wiki/YouTube#Quality_and_formats || NO LONGER IN WIKIPEDIA, use older image

        // Video and Audio
        FORMAT_MAP.put(17, new Format(17, "3gp", 144, Format.VCodec.MPEG4, Format.ACodec.AAC, 24, false));
        FORMAT_MAP.put(36, new Format(36, "3gp", 240, Format.VCodec.MPEG4, Format.ACodec.AAC, 32, false));
        FORMAT_MAP.put(5, new Format(5, "flv", 240, Format.VCodec.H263, Format.ACodec.MP3, 64, false));
        FORMAT_MAP.put(43, new Format(43, "webm", 360, Format.VCodec.VP8, Format.ACodec.VORBIS, 128, false));
        FORMAT_MAP.put(18, new Format(18, "mp4", 360, Format.VCodec.H264, Format.ACodec.AAC, 96, false));
        FORMAT_MAP.put(22, new Format(22, "mp4", 720, Format.VCodec.H264, Format.ACodec.AAC, 192, false));

        // Dash Video
        FORMAT_MAP.put(160, new Format(160, "mp4", 144, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(133, new Format(133, "mp4", 240, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(134, new Format(134, "mp4", 360, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(135, new Format(135, "mp4", 480, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(136, new Format(136, "mp4", 720, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(137, new Format(137, "mp4", 1080, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(264, new Format(264, "mp4", 1440, Format.VCodec.H264, Format.ACodec.NONE, true));
        FORMAT_MAP.put(266, new Format(266, "mp4", 2160, Format.VCodec.H264, Format.ACodec.NONE, true));

        FORMAT_MAP.put(298, new Format(298, "mp4", 720, Format.VCodec.H264, 60, Format.ACodec.NONE, true));
        FORMAT_MAP.put(299, new Format(299, "mp4", 1080, Format.VCodec.H264, 60, Format.ACodec.NONE, true));

        // Dash Audio
        FORMAT_MAP.put(140, new Format(140, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 128, true));
        FORMAT_MAP.put(141, new Format(141, "m4a", Format.VCodec.NONE, Format.ACodec.AAC, 256, true));

        // WEBM Dash Video
        FORMAT_MAP.put(278, new Format(278, "webm", 144, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(242, new Format(242, "webm", 240, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(243, new Format(243, "webm", 360, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(244, new Format(244, "webm", 480, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(247, new Format(247, "webm", 720, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(248, new Format(248, "webm", 1080, Format.VCodec.VP9, Format.ACodec.NONE, true));
        FORMAT_MAP.put(271, new Format(271, "webm", 1440, Format.VCodec.VP9, Format.ACodec.NONE, true));
        
        FORMAT_MAP.put(272, new Format(272, "webm", 4320, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, false));
        
        FORMAT_MAP.put(313, new Format(313, "webm", 2160, Format.VCodec.VP9, Format.ACodec.NONE, true));

        // 336 = 1440p60: yuv420p10
        FORMAT_MAP.put(336, new Format(336, "webm", 1440, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        //335 = 1080p60: yuv420p10
        FORMAT_MAP.put(335, new Format(335, "webm", 1080, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(333, new Format(333, "webm", 480, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(334, new Format(334, "webm", 720, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(332, new Format(332, "webm", 360, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(331, new Format(331, "webm", 240, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(330, new Format(330, "webm", 144, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        FORMAT_MAP.put(337, new Format(337, "webm", 4320, Format.VCodec.VP9, 60, Format.ACodec.NONE, true, true));
        
        //end of yuv420p10 (10 bit coloured streams)

        FORMAT_MAP.put(302, new Format(302, "webm", 720, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
        FORMAT_MAP.put(308, new Format(308, "webm", 1440, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
        FORMAT_MAP.put(303, new Format(303, "webm", 1080, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));
        FORMAT_MAP.put(315, new Format(315, "webm", 2160, Format.VCodec.VP9, 60, Format.ACodec.NONE, true));

        // WEBM Dash Audio
        FORMAT_MAP.put(171, new Format(171, "webm", Format.VCodec.NONE, Format.ACodec.VORBIS, 128, true));

        FORMAT_MAP.put(249, new Format(249, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 48, true));
        FORMAT_MAP.put(250, new Format(250, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 64, true));
        FORMAT_MAP.put(251, new Format(251, "webm", Format.VCodec.NONE, Format.ACodec.OPUS, 160, true));

        // HLS Live Stream
        
        FORMAT_MAP.put(91, new Format(91, "mp4", 144 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
        FORMAT_MAP.put(92, new Format(92, "mp4", 240 ,Format.VCodec.H264, Format.ACodec.AAC, 48, false, true));
        FORMAT_MAP.put(93, new Format(93, "mp4", 360 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
        FORMAT_MAP.put(94, new Format(94, "mp4", 480 ,Format.VCodec.H264, Format.ACodec.AAC, 128, false, true));
        FORMAT_MAP.put(95, new Format(95, "mp4", 720 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
        FORMAT_MAP.put(96, new Format(96, "mp4", 1080 ,Format.VCodec.H264, Format.ACodec.AAC, 256, false, true));
        
        // AVO1/AV1 videos (Video ONLY, no audio)
        
        FORMAT_MAP.put(394, new Format(394, "mp4", 144, VCodec.AV1, ACodec.NONE, false));
        FORMAT_MAP.put(395, new Format(395, "mp4", 240, VCodec.AV1, ACodec.NONE, false));
        FORMAT_MAP.put(396, new Format(396, "mp4", 360, VCodec.AV1, ACodec.NONE, false));
        FORMAT_MAP.put(397, new Format(397, "mp4", 480, VCodec.AV1, ACodec.NONE, false));
        FORMAT_MAP.put(398, new Format(398, "mp4", 720, VCodec.AV1, ACodec.NONE, false));
        FORMAT_MAP.put(399, new Format(399, "mp4", 1080, VCodec.AV1, ACodec.NONE, false));
    }

    public YouTubeExtractor() {
    }

    protected void onPostExecute(final HashMap<Integer, YtFile> ytFiles) {
        this.onExtractionComplete(ytFiles, this.videoMeta);
    }
    
    private int index;
    private boolean forceEnglish;
    
    /**
     * String used to enforce English (United Kingdom)
     */
    public static final String HL_GB = "&hl=gb";
    /**
     * String used to enforce English (United States)
     */
    public static final String HL_EN = "&hl=en";
    
    /**
     * Start the extraction.
     *
     * @param youtubeLink       the youtube page link or video id
     * @param parseDashManifest true if the dash manifest should be downloaded and parsed
     * @param includeWebM       true if WebM streams should be extracted
     * @param fastMode 			 true if fast scraper should be used instead
     */
    public void extract(String youtubeLink, boolean parseDashManifest, boolean includeWebM, final boolean fastMode) {
    	this.extract(youtubeLink, parseDashManifest, includeWebM, fastMode, -1);
    }

    /**
     * Start the extraction.
     *
     * @param youtubeLink       the youtube page link or video id
     * @param parseDashManifest true if the dash manifest should be downloaded and parsed
     * @param includeWebM       true if WebM streams should be extracted
     * @param fastMode 			 true if fast scraper should be used instead
     */
    public void extract(String youtubeLink, boolean parseDashManifest, boolean includeWebM, final boolean fastMode, final int index) {
        this.parseDashManifest = parseDashManifest;
        this.includeWebM = includeWebM;
        this.fastMode = fastMode;
        final boolean tp = JVMUtil.isArg("--trace");
        this.logger.traceEnabled = tp;
        this.logger.debugEnabled = tp;
        this.index = index;
        //final HashMap<Integer, YtFile> t = this.doInBackground(youtubeLink.replaceAll(Pattern.quote("hooktube.com"), "youtube.com"));
        //this.onExtractionComplete(t, this.videoMeta);
        Promiser.promise(new Promise<HashMap<Integer, YtFile>>() {

			@Override
			public HashMap<Integer, YtFile> run() throws Throwable {
				return YouTubeExtractor.this.doInBackground(
						youtubeLink.replaceAll(Pattern.quote("hooktube.com"), "youtube.com"));
			}

			@Override
			public void result(final HashMap<Integer, YtFile> result) {
				YouTubeExtractor.this.onExtractionComplete(result, YouTubeExtractor.this.videoMeta);
			}

			@Override
			public void error(final Throwable throwable) {
				if (throwable != null) {
					throwable.printStackTrace();
				}else {
					JVMUtil.async_error("Blank exception thrown!");
				}
				this.result(null);
			}
		});
    }

    protected abstract void onExtractionComplete(final HashMap<Integer, YtFile> ytFiles, final VideoMeta videoMeta);

    private String list = null;
    
    private long start = 10000000L;
    private boolean fastMode;
    protected HashMap<Integer, YtFile> doInBackground(String... params) {
    	this.start = System.currentTimeMillis();
        videoID = null;
        String ytUrl = params[0];
    	final Map<String, List<String>> s = URIUtil2.splitQuery(URIUtil.safeCreate(ytUrl));
    	if (s.containsKey("list") && s.get("list").size() != 0) {
    		this.list = s.get("list").get(0);
    	}
        if (ytUrl == null) {
        	logger.trace("doInBackground(params) --> ytURL == null.");
            return null;
        }
        Matcher mat = patYouTubePageLink.matcher(ytUrl);
        if (mat.find()) {
            videoID = mat.group(3);
        } else {
            mat = patYouTubeShortLink.matcher(ytUrl);
            if (mat.find()) {
                videoID = mat.group(3);
            } else if (ytUrl.matches("\\p{Graph}+?")) {
                videoID = ytUrl;
            }
        }
        logger.trace("Time taken to process URL: " + (System.currentTimeMillis() - start));
        if (videoID != null) {
            try {
//                ThreadExecutor.execute(new Runnable() {
//				//	
//					@Override
//					public void run() {
//						ArrayUtils.printLln(YTComments.getComments(videoID));
//					}
//				});
                //ArrayUtils.printLln(Parser.parse(ytUrl));
                return getStreamUrls();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            logger.error(LOG_TAG, "Wrong YouTube link format");
        }
        logger.trace("doInBackground(params) --> Not parsed.");
        return null;
    }
    
    public boolean __debug_emulate_country_block;
    
    private HashMap<Integer, YtFile> getStreamUrls() throws IOException, InterruptedException {

        String ytInfoUrl = (useHttp) ? "http://" : "https://";
        ytInfoUrl += "www.youtube.com/get_video_info?video_id=" + videoID + "&eurl="
                + URLEncoder.encode("https://youtube.googleapis.com/v/" + videoID, "UTF-8");
        
        if (this.forceEnglish) {
        	ytInfoUrl += this.HL_EN;
        }
        
        // System.out.println("ytInfoUrl = " + ytInfoUrl);
        if (LOGGING) {
        	logger.debug("Video.ytInfoURL = " + ytInfoUrl);
        }
        
        //TODO: Move to one info, use URIUtil.downloadPage();
        String dashMpdUrl = null;
        String streamMap;
        String pageMap = null;
        BufferedReader reader = null;
        URL getUrl = new URL(ytInfoUrl);
        if(LOGGING)
            logger.debug(LOG_TAG, "infoUrl: " + ytInfoUrl);
        
        boolean localWatchHack = false;
        
        Proxy proxy = Proxy.NO_PROXY;
        //ScrapedProxy sproxy = null;
        boolean verify_controversy = false;
        
        try {
        	// look more realisitic
        	Thread.sleep(MathUtil.randInt(150, 300));
        }catch (Exception e) {}
        final String STREAM_MAP_NEW = URIUtil.downloadPage(getUrl, true, proxy);
        
        try {
        	final String vID = "https://www.youtube.com/watch?v=" + this.videoID + (this.list != null ? "&list=" + this.list : "")
        			+ (this.forceEnglish ? this.HL_EN : "");
            //logger.debug("Attempting playback lock bypass...");
            pageMap = URIUtil.downloadPage(URIUtil.safeCreate(vID)); //.replaceAll(Pattern.quote("\\u0026"), "&");
            if (pageMap.contains("This content is not available on this country domain.")
            		|| STREAM_MAP_NEW.contains("subreason=The+uploader+has+not+made+this+video+available+in+your+country.") || 
            		this.__debug_emulate_country_block || 
            		pageMap.contains("who has blocked it in your country on copyright grounds")
            		|| (pageMap.contains("<meta itemprop=\"regionsAllowed\" content=\"\">") && pageMap.contains("<div id=\"limited-state-header\">"))) {
            	logger.debug("Attempting country block bypass...");
            	//ScrapedProxy random = ArrayUtils.random(proxies);
            	//for (final ScrapedProxy proxy0 : proxies) {
            	//	if (proxy0.getCC().equals("US")) {
            	//		random = proxy0;
            	//		break;
            	//	}
            	//}
            	//logger.debug("Using proxy in country by code of " + random.getCC() + ". [proxy=" + random.toString() + "]");
            	pageMap = URIUtil.downloadPage(URIUtil.safeCreate(vID), false);
            	//sproxy = random;
            	//proxy = random.toProxy();
            }
            if (pageMap.contains("verify_controversy")) {
            	verify_controversy = true;
            	logger.debug("Limited video detected: Updating pageMap...");
            	pageMap = URIUtil.downloadPage(URIUtil.safeCreate(vID + "&bpctr=" + System.currentTimeMillis()));
            }//else if (pageMap.contains("og:restrictions:age")) {
            //	logger.debug("Age restricted video detected: Updating pageMap...");
            //	pageMap = URIUtil.downloadPage(URIUtil.safeCreate(vID + "&has_verified=1"));
            //}
        }catch (Exception e) {}
        
        //FIXME: Fix controversy issues.
        if (verify_controversy) {
        	logger.debug("Limited video detected: Updating API url...");
        	getUrl = new URL(ytInfoUrl + "&bpctr=" + System.currentTimeMillis());
        }
        
        //TODO: remove source of slowdown, try to reach limit of 3s.
        HttpURLConnection urlConnection = (HttpURLConnection) getUrl.openConnection(proxy);
        urlConnection.setRequestProperty("User-Agent", USER_AGENT);
        urlConnection.setRequestProperty("Host", "www.youtube.com");
        urlConnection.setRequestProperty("Referer", "https://www.youtube.com/watch?v=" + videoID);
        urlConnection.setRequestProperty("X-SPF-Referer", "https://www.youtube.com/watch?v=" + videoID);
        urlConnection.setRequestProperty("X-SPF-Previous", "https://www.youtube.com/watch?v=" + videoID);
        urlConnection.setConnectTimeout(10000);
        try {
            reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
            streamMap = reader.readLine();
            //System.out.println(streamMap);
            if (streamMap.contains("Playback+on+other+websites+has+been+disabled+by+the+video+owner.")
            		|| streamMap.contains("&status=fail")) {
            	localWatchHack = true;
            	logger.debug("Using embed block bypass...");
            }else if (streamMap.contains("This%20video%20can%27t%20be%20played%20here%2C%20but%20you%20can%20still%20watch%20it%20on%20YouTube.")) {
            	localWatchHack = true;
            	logger.debug("Using embed block bypass...");
            }
        }catch (Exception e) {
        	e.printStackTrace();
        	return new HashMap<Integer, YtFile>();
        } finally {
            if (reader != null)
                reader.close();
            urlConnection.disconnect();
        }
        logger.trace("Time taken to download metadata: " + (System.currentTimeMillis() - start));
        Matcher mat;
        String curJsFileName = null;
        String[] streams;
        HashMap<Integer, String> encSignatures = null;

       /* logger.debug("FastMode2: Accquire signature decrypter...");
		logger.debug("FastMode2: ASD: Getting player.js...");
		String jsData = null;
		try {
			String ex = StringsUtil.substringBetweenO(pageMap, "<script src=\"/yts/jsbin/player-", "\"");
			if (ex == null) {
				final String newPageInfo = URIUtil.downloadPage(URIUtil.safeCreate(
    					"https://www.youtube.com/watch?v=" + videoID + "&bpctr=" + System.currentTimeMillis()
    					+ (this.list != null ? "&list=" + this.list : "") +
    					(this.forceEnglish ? this.HL_EN : "")), //"https://www.youtube.com/verify_controversy?next_url=/" + (URIUtil.safeEncode("watch?v=" + videoID))), 
        				false, proxy);
				pageMap = newPageInfo;
    			ex = StringsUtil.substringBetweenO(pageMap, "<script src=\"/yts/jsbin/player-", "\"");
			}
			boolean usingFix = false;
			if (ex == null && pageMap.contains("/yts/jsbin/player_ias")) {
				ex = StringsUtil.substringBetweenO(pageMap,
						"<script src=\"/yts/jsbin/player_", "\"");
				usingFix = true;
			}
			if (ex == null) {
				logger.error("Unable to get player JS!");
				//if (this.logger.traceEnabled) {
				//	this.logger.trace("Printing HTML...");
				////	this.logger.debugC(pageMap);
				//}
				logger.info("*bzzzzzzzzzzzzzzzzzz* restart et");
				return this.getStreamUrls();
			}
			String url = "https://s.ytimg.com/yts/jsbin/player-" +
    				ex;
			if (usingFix) {
				url = "https://s.ytimg.com/yts/jsbin/player_" +
	    				ex;
			}
			logger.debug("FastMode2: Player URL: " + url);
			jsData = URIUtil.downloadPage(
    				URIUtil.safeCreate(url));
		}catch (IOException e) {
			e.printStackTrace();
			final HashMap<Integer, YtFile> __temp = new HashMap<Integer, YtFile>(0);
			if (this.videoMeta != null) {
        		videoMeta.setErrorString("Unable to get signature decryption method. You should try again.");
			}
			return __temp;
		}*/
        if (decrypter.get() == null) {
        	logger.error("Signature decrypter hasn't been accquired yet, waiting it out...");
        	while (decrypter.get() == null) {
        		Thread.sleep(200L);
        	}
        }
		final YTSignatureDecrypter sigDec = 
				(!JVMUtil.isArg("--Signatures.useSoftwareJS") ? decrypter.get() : null);
		final YTSigSoftwareDecoder sigSoftDec = (sigDec != null ? 
				new YTSigSoftwareDecoder(sigDec.functionName(),
						sigDec.getCode()) : null /* YTSig.accquireSoft(jsData) */);
		//if (sigSoftDec != null) {
		//	sigSoftDec.debug();
		//}
        
        final LinkedList<DataSet<Integer, String>> results = parseVideoMeta(streamMap, pageMap, localWatchHack, 
        		proxy, index, verify_controversy, sigDec, sigSoftDec);
        //System.out.println(results);
        // fastMode for Streams Only
        if (results != null && results.size() == 1 && (videoMeta.isLiveStream() || results.get(0).getA() == 1)) {
        	final HashMap<Integer, YtFile> out = new HashMap<Integer, YtFile>();
        	final DataSet<Integer, String> o = results.get(0);
        	prevVideo = videoID;
        	out.put(o.getA(), new YtFile(new Format(
        			o.getA(), "hls", 720, VCodec.H264, ACodec.AAC, 48000, false, true), o.getB()));
        	return out;
        }else if (results != null && !results.isEmpty() && this.fastMode) {
        	final HashMap<Integer, YtFile> out = new HashMap<Integer, YtFile>();
        	for (final DataSet<Integer, String> ds : results) {
        		VCodec vc = VCodec.NONE;
        		int height = -1;
        		int fps = 30;
        		int br = -1;
        		String ext = "";
        		if (ds.getB() == null) {
        			continue;
        		}
//        		if (ds.getB().contains("video/webm")) {
//        			vc = VCodec.VP9;
//        			ext = "video/webm";
//        		}else if (ds.getB().contains("video/mp4")) {
//        			vc = (ds.getB().contains("codecs=\"av1\"") ? VCodec.AV1 : VCodec.MPEG4);
//        			ext = "video/mp4";
//        		}else {
//        			final Format f = getFormat(ds.getA());
//        			if (f != null) {
//        				vc = f.getVideoCodec();
//            			ext = f.getExt();
//            			height = f.getHeight();
//            			fps = f.getFps();
//        			}
//        		}
        		final Format form = getFormat(ds.getA());
    			if (form != null) {
    				vc = form.getVideoCodec();
        			ext = form.getExt();
        			height = form.getHeight();
        			fps = form.getFPS();
    			}
        		ACodec ac = ACodec.NONE;
    			if (form != null) {
    				ext = (form.getExt() != null ? form.getExt() : "");
        			ac = form.getAudioCodec();
        			br = form.getAudioBitrate();
    			}else {
    			}
//        		if (ds.getB().contains("audio/webm")) {
//        			ac = ACodec.OPUS;
//        			ext = "audio/webm";
//        		}else if (ds.getB().contains("audio/mp4")) {
//        			ac = ACodec.AAC;
//        			ext = "audio/mp4";
//        		}else {
//        			final Format f = getFormat(ds.getA());
//        			if (f != null) {
//        				ext = (f.getExt() != null ? f.getExt() : "");
//            			ac = f.getAudioCodec();
//        			}else {
//        			}
//        		}
        		final Format f = new Format(ds.getA(), ext,
        				height, vc, fps, ac, br, true, form.is10bit());
        		final YtFile ytf = new YtFile(f, ds.getB());
        		if (JVMUtil.isArg("--FastMode.debug")) {
        			logger.debug("FastMode2.output: " + ytf);
        		}
        		out.put(ds.getA(), ytf);
        	}
        	logger.trace("Time taken to exit FastMode2 processing: " + (System.currentTimeMillis() - start));
        	prevVideo = videoID;
        	return out;
        }else if (JVMUtil.isArg("--onlyFastMode") && results.isEmpty()) {
        	logger.error("Failed to extract any videos via fast mode! (OnlyFastMode is enforced)");
        	final HashMap<Integer, YtFile> out = new HashMap<Integer, YtFile>();
        	return out;
        }

        if (videoMeta.isLiveStream()) {
            mat = patHlsvp.matcher(streamMap);
            if(mat.find()) {
                String hlsvp = URLDecoder.decode(mat.group(1), "UTF-8");
                HashMap<Integer, YtFile> ytFiles = new HashMap<Integer, YtFile>();

                getUrl = new URL(hlsvp);
                urlConnection = (HttpURLConnection) getUrl.openConnection(proxy);
                urlConnection.setRequestProperty("User-Agent", USER_AGENT);
                try {
                    reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    String line;
                    while ((line = reader.readLine()) != null) {
                       if(line.startsWith("https://") || line.startsWith("http://")){
                           mat = patHlsItag.matcher(line);
                           if(mat.find()){
                               int itag = Integer.parseInt(mat.group(1));
                               YtFile newFile = new YtFile(FORMAT_MAP.get(itag), line);
                               ytFiles.put(itag, newFile);
                           }
                       }
                    }
                } finally {
                    if (reader != null)
                        reader.close();
                    urlConnection.disconnect();
                }

                if (ytFiles.size() == 0) {
                    if (LOGGING)
                        logger.debug(LOG_TAG, streamMap);
                    logger.error("extract().failed() -> No HLS streams found.");
                    return null;
                }
                return ytFiles;
            }
            logger.error("extract().failed() -> No HLS found. Attempting to continue...");
        }

        // "use_cipher_signature" disappeared, we check whether at least one ciphered signature
        // exists in the stream_map.
        boolean sigEnc = true;
        if (streamMap != null && streamMap.contains(STREAM_MAP_STRING)){
            String streamMapSub = streamMap.substring(streamMap.indexOf(STREAM_MAP_STRING));
            mat = patIsSigEnc.matcher(streamMapSub);
            //fixed by FastMode2
            final Matcher m = patIsSigEnc.matcher(STREAM_MAP_NEW);
            if (!mat.find() && !m.find()) {
                sigEnc = false;
            }
            logger.trace("Time taken to identify cipher requirements: " + (System.currentTimeMillis() - start));
        }

        // Some videos are using a ciphered signature we need to get the
        // deciphering js-file from the youtube page.
        if (sigEnc) {
            // Get the video directly from the youtubepage
            if (CACHING
                    && (decipherJsFileName == null || decipherFunctions == null || decipherFunctionName == null)) {
                readDecipherFunctFromCache();
            }
            getUrl = (verify_controversy ? new URL("https://youtube.com/watch?v=" + videoID + "&bpctr=" + System.currentTimeMillis())
            		: new URL("https://youtube.com/watch?v=" + videoID));
            urlConnection = (HttpURLConnection) getUrl.openConnection(proxy);
            urlConnection.setRequestProperty("User-Agent", USER_AGENT);
            try {
                reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                    // logger.debug("line", line);
                    if (line.contains(STREAM_MAP_STRING)) {
                        streamMap = line.replace("\\u0026", "&");
                        break;
                    }
                }
                logger.trace("Time taken to register stream map [via decipher]: " + (System.currentTimeMillis() - start));
            } finally {
                if (reader != null)
                    reader.close();
                urlConnection.disconnect();
            }
            encSignatures = new HashMap<Integer, String>();

            //FIXME: Remove these, waste of time, already done by YtSig.class
            //TODO: Put the older decipherer to sleep, one final time.
            
            mat = patDecryptionJsFile.matcher(streamMap);
            if (mat.find()) {
                curJsFileName = mat.group(1).replace("\\/", "/");
                if (decipherJsFileName == null || !decipherJsFileName.equals(curJsFileName)) {
                    decipherFunctions = null;
                    decipherFunctionName = null;
                }
                decipherJsFileName = curJsFileName;
            }
            mat = patDecryptionJsFile.matcher(streamMap);
            if (mat.find()) {
                curJsFileName = mat.group(1).replace("\\/", "/");
                if (decipherJsFileName == null || !decipherJsFileName.equals(curJsFileName)) {
                    decipherFunctions = null;
                    decipherFunctionName = null;
                }
                decipherJsFileName = curJsFileName;
            }
            logger.trace("Time taken to get JS File Name: " + (System.currentTimeMillis() - start));

            if (parseDashManifest) {
                mat = patDashManifest2.matcher(streamMap);
                if (mat.find()) {
                    dashMpdUrl = mat.group(1).replace("\\/", "/");
                    mat = patDashManifestEncSig.matcher(dashMpdUrl);
                    if (mat.find()) {
                        encSignatures.put(0, mat.group(1));
                    } else {
                        dashMpdUrl = null;
                    }
                }
                logger.trace("Time taken to process DASH manifest: " + (System.currentTimeMillis() - start));
            }
            logger.trace("Time taken to parse decryption of video: " + (System.currentTimeMillis() - start));
        } else {
            if (parseDashManifest) {
                mat = patDashManifest1.matcher(streamMap);
                if (mat.find()) {
                    dashMpdUrl = URLDecoder.decode(mat.group(1), "UTF-8");
                }
            }
            streamMap = URLDecoder.decode(streamMap, "UTF-8");
            logger.trace("Time taken to load video: " + (System.currentTimeMillis() - start));
        }

        streams = streamMap.split(",|"+STREAM_MAP_STRING+"|&adaptive_fmts=");
        if (streams.length == 0) {
        	streams = URIUtil.safeDecode(STREAM_MAP_NEW).split(",|"+STREAM_MAP_STRING+"|&adaptive_fmts=");
        	if (streams.length == 0) {
        		if (this.videoMeta != null) {
            		this.videoMeta.setErrorString("YouTube machine broke. Please try again. (Error: BLANK_STREAMS_TO_PROCESS)");
            		this.videoMeta.setAdvancedErrorString("There were no video streams detected.");
            	}else {
            		this.logger.error("Unable to inform user about error BLANK_STREAMS_TO_PROCESS");
            	}
            	return null;
        	}
        }
        HashMap<Integer, YtFile> ytFiles = new HashMap<Integer, YtFile>();
        for (String encStream : streams) {
            encStream = encStream + ",";
            if (!encStream.contains("itag%3D")) {
                continue;
            }
            String stream;
            stream = URLDecoder.decode(encStream, "UTF-8");

            mat = patItag.matcher(stream);
            int itag;
            if (mat.find()) {
                itag = Integer.parseInt(mat.group(1));
                if (LOGGING)
                    logger.debug(LOG_TAG, "Itag found:" + itag);
                if (FORMAT_MAP.get(itag) == null) {
                    if (LOGGING)
                        logger.debug(LOG_TAG, "Itag not in list:" + itag);
                    continue;
                } else if (!includeWebM && FORMAT_MAP.get(itag).getExt().equals("webm")) {
                    continue;
                }
            } else {
                continue;
            }

            if (curJsFileName != null) {
                mat = patEncSig.matcher(stream);
                if (mat.find()) {
                    encSignatures.put(itag, mat.group(1));
                }
            }
            mat = patUrl.matcher(encStream);
            String url = null;
            if (mat.find()) {
                url = mat.group(1);
            }

            if (url != null) {
                Format format = FORMAT_MAP.get(itag);
                String finalUrl = URLDecoder.decode(url, "UTF-8");
                YtFile newVideo = new YtFile(format, finalUrl);
                ytFiles.put(itag, newVideo);
                //if (URIUtil.hasContent(URIUtil.safeCreate(newVideo.getUrl()))) {
               // 	ytFiles.put(itag, newVideo);
                //}
            }
        }
        logger.trace("Time taken to process stream: " + (System.currentTimeMillis() - start));

        if ((encSignatures == null || 
        		encSignatures.isEmpty()) && sigEnc) {
        	if (!JVMUtil.isArg("--disableBogging")) {
        		logger.error("*bzzzz* restart eet. (Restarting extraction due to no signatures)");
        		return this.getStreamUrls();
        	}
        	if (this.videoMeta != null) {
        		this.videoMeta.setErrorString("YouTube machine broke. Please try again.");
        		this.videoMeta.setAdvancedErrorString("BLANK_SIGNATURE_LIST | The signature list to decrypt was blank.");
        	}else {
        		this.logger.error("Unable to inform user about error BLANK_SIGNATURE_LIST");
        	}
        	if (!JVMUtil.isArg("--disableBogging")) {
        		return null;
        	}
        }
        if (encSignatures != null) {
            if (LOGGING)
                logger.debug(LOG_TAG, "Decipher signatures");
            if (this.logger.traceEnabled && encSignatures.size() < 8) {
            	logger.trace("Printing encSignatures: ");
            	ArrayUtils.printLln(encSignatures);
            }
            String signature;
            decipheredSignature = null;
            if (this.decipherSignature(encSignatures, proxy, sigDec)) {
                lock.lock();
                try {
                    jsExecuting.await(7, TimeUnit.SECONDS);
                } finally {
                    lock.unlock();
                }
            }
            logger.trace("Time taken to exeucte full decipher: " + (System.currentTimeMillis() - start));
            signature = decipheredSignature;
            if (signature == null) {
            	logger.error("extract().failed() -> No signature found.");
            	if (videoMeta != null) {
            		videoMeta.setErrorString("Couldn't generate a signature, you should reload to try again.");
            	}
                return null;
            } else {
            	logger.debug("extract().sigDebug() -> Signatures: \n" + signature);
                String[] sigs = signature.split("\n");
                for (int i = 0; i < encSignatures.size() && i < sigs.length; i++) {
                    int key = ArrayUtils.indexOfA(encSignatures, i); //i; //encSignatures.keyAt(i);
                    if (key == 0) {
                        dashMpdUrl = dashMpdUrl.replace("/s/" + encSignatures.get(key), "/signature/" + sigs[i]);
                    } else {
                    	final YtFile i0 = ytFiles.get(key);
                    	if (i0 == null) {
                    		logger.warn("extract().sig().addSig() -> A YTFile was null! [YTFiles.size=" + ytFiles.size() + "]");
                    		continue;
                    	}
                        String url = i0.getUrl();
                        url += "&signature=" + sigs[i];
                        YtFile newFile = new YtFile(FORMAT_MAP.get(key), url);
                       //if (URIUtil.hasContent(URIUtil.safeCreate(newFile.getUrl()))) {
                        	ytFiles.put(key, newFile);
                       // }
                    }
                }
                logger.trace("Time taken to add decoded URLs: " + (System.currentTimeMillis() - start));
            }
            logger.trace("Time taken to decipher video: " + (System.currentTimeMillis() - start));
        }

        if (parseDashManifest && dashMpdUrl != null) {
            for (int i = 0; i < DASH_PARSE_RETRIES; i++) {
                try {
                    // It sometimes fails to connect for no apparent reason. We just retry.
                    parseDashManifest(dashMpdUrl, ytFiles, proxy);
                    break;
                } catch (IOException io) {
                    Thread.sleep(5);
                    if (LOGGING)
                        logger.debug(LOG_TAG, "Failed to parse dash manifest " + (i + 1));
                }
            }
            logger.trace("Time taken to parse DASH: " + (System.currentTimeMillis() - start));
        }

        if (ytFiles.size() == 0) {
        	if (streamMap.contains("status=fail")) {
        		if (videoMeta != null && pageMap.contains("<meta itemprop=\"regionsAllowed\" content=\"\">")) {
        			videoMeta.setErrorString("YouTube has censored this video, what did you expect?");
            		videoMeta.setAdvancedErrorString("ERROR_EXTRACTION_CENSORSHIP | (((YouTube))) prevented us from extracting the video. [Failed after proxied attempts]");
        		}else if (videoMeta != null && !streamMap.contains("&reason=")) {
            		videoMeta.setErrorString("Couldn't get ANY video information. Please try again. (Probably embed blocked)");
            		videoMeta.setAdvancedErrorString("ERROR_INVALID_STREAM_MAP | The stream map returned an errornous status code.");
            	}else if (videoMeta != null && streamMap.contains("&reason=")) {
            		String f = StringsUtil.substringBetweenO(streamMap, "&reason=", "&");
            		f = f.replaceAll(Pattern.quote("+"), " ");
            		f = URIUtil.safeDecode(f);
            		videoMeta.setErrorString(f);
            		videoMeta.setAdvancedErrorString("ERROR_EXTRACTION_GENERAL_FAILURE | (((YouTube))) prevented us from extracting the video.");
            	}
                return null;
        	}
            if (LOGGING)
                logger.debug("Stream Map: " + streamMap);
            logger.error("extract().failed() -> Final result was blank.");
            return null;
        }
        logger.trace("Time taken finish getYTFiles(): " + (System.currentTimeMillis() - start));
        prevVideo = videoID;
        return ytFiles;
    }

    private Format getFormat(final int itag) {
		return FORMAT_MAP.get(itag);
	}

	private boolean decipherSignature(final HashMap<Integer, String> encSignatures, final Proxy proxy
			, final YTSignatureDecrypter sigDec) throws IOException {
        // Assume the functions don't change that much
        if ((decipherFunctionName == null || decipherFunctions == null) && sigDec == null) {
        	if (decipherJsFileName == null) {
        		return false;
        	}
            String decipherFunctUrl = "https://s.ytimg.com/yts/jsbin/" + decipherJsFileName;

            if (LOGGING)
                logger.debug(LOG_TAG, "Decipher FunctURL: " + decipherFunctUrl);
           
            if (JVMUtil.isArg("--youtubeExtractor.useLegacySigExtraction")) {
            	 BufferedReader reader = null;
                 String javascriptFile;
                 final URL url = new URL(decipherFunctUrl);
                 final HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(proxy);
                 urlConnection.setRequestProperty("User-Agent", USER_AGENT);
                 try {
                     reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                     StringBuilder sb = new StringBuilder("");
                     String line;
                     while ((line = reader.readLine()) != null) {
                         sb.append(line);
                         sb.append(" ");
                     }
                     javascriptFile = sb.toString();
                 } finally {
                     if (reader != null)
                         reader.close();
                     urlConnection.disconnect();
                 }
                 
                 Matcher mat = patSignatureDecFunction.matcher(javascriptFile);
            	
            	if (!mat.find()) {
            		return false;
            	}
                decipherFunctionName = mat.group(1);
                if (LOGGING)
                    logger.debug(LOG_TAG, "Decipher Functname: " + decipherFunctionName);

                Pattern patMainVariable = Pattern.compile("(var |\\s|,|;)" + decipherFunctionName.replace("$", "\\$") +
                        "(=function\\((.{1,3})\\)\\{)");

                String mainDecipherFunct;

                mat = patMainVariable.matcher(javascriptFile);
                if (mat.find()) {
                    mainDecipherFunct = "var " + decipherFunctionName + mat.group(2);
                } else {
                    Pattern patMainFunction = Pattern.compile("function " + decipherFunctionName.replace("$", "\\$") +
                            "(\\((.{1,3})\\)\\{)");
                    mat = patMainFunction.matcher(javascriptFile);
                    if (!mat.find())
                        return false;
                    mainDecipherFunct = "function " + decipherFunctionName + mat.group(2);
                }

                int startIndex = mat.end();

                for (int braces = 1, i = startIndex; i < javascriptFile.length(); i++) {
                    if (braces == 0 && startIndex + 5 < i) {
                        mainDecipherFunct += javascriptFile.substring(startIndex, i) + ";";
                        break;
                    }
                    if (javascriptFile.charAt(i) == '{')
                        braces++;
                    else if (javascriptFile.charAt(i) == '}')
                        braces--;
                }
                decipherFunctions = mainDecipherFunct;
                // Search the main function for extra functions and variables
                // needed for deciphering
                // Search for variables
                mat = patVariableFunction.matcher(mainDecipherFunct);
                while (mat.find()) {
                    String variableDef = "var " + mat.group(2) + "={";
                    if (decipherFunctions.contains(variableDef)) {
                        continue;
                    }
                    startIndex = javascriptFile.indexOf(variableDef) + variableDef.length();
                    for (int braces = 1, i = startIndex; i < javascriptFile.length(); i++) {
                        if (braces == 0) {
                            decipherFunctions += variableDef + javascriptFile.substring(startIndex, i) + ";";
                            break;
                        }
                        if (javascriptFile.charAt(i) == '{')
                            braces++;
                        else if (javascriptFile.charAt(i) == '}')
                            braces--;
                    }
                }
                // Search for functions
                mat = patFunction.matcher(mainDecipherFunct);
                while (mat.find()) {
                    String functionDef = "function " + mat.group(2) + "(";
                    if (decipherFunctions.contains(functionDef)) {
                        continue;
                    }
                    startIndex = javascriptFile.indexOf(functionDef) + functionDef.length();
                    for (int braces = 0, i = startIndex; i < javascriptFile.length(); i++) {
                        if (braces == 0 && startIndex + 5 < i) {
                            decipherFunctions += functionDef + javascriptFile.substring(startIndex, i) + ";";
                            break;
                        }
                        if (javascriptFile.charAt(i) == '{')
                            braces++;
                        else if (javascriptFile.charAt(i) == '}')
                            braces--;
                    }
                }

                if (LOGGING)
                    logger.debug(LOG_TAG, "Decipher Function: " + decipherFunctions);
                decipherViaWebView(encSignatures);
                if (CACHING) {
                    writeDeciperFunctToChache();
                }
            } else {
                logger.debug("YouTubeExtractor.sigBack: Trying new method...");
              //  final Matcher sigFunc = Pattern.compile("(\\w+)\\s*=\\s*function\\((\\w+)\\){\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;",
              //  		Pattern.MULTILINE)
              //  		.matcher(javascriptFile);
              //  if (!sigFunc.find()) {
              //  	logger.debug("YouTubeExtractor.sigBack: METHOD was not found.");
             //   	return false;
            //    }
                /*
                final String subStringToValue = StringsUtil.substringBetweenO(javascriptFile, "=function(a){a=a.split(\"\");",
                		";return a.join(\"\")};");
                if (subStringToValue == null) {
                    logger.debug("YouTubeExtractor.sigBack: METHOD was not found.");
                    return false;
                }
                logger.debug("YouTubeExtractor.sigBack: Result of new method: " + subStringToValue);
                logger.debug("Back tracking to get function name...");
                final int index = javascriptFile.indexOf(subStringToValue);
                logger.debug("YouTubeExtractor.sigBack.traceBack: Index: " + index);
                final String r = javascriptFile.substring(index - "=function(a){a=a.split(\"\");".length() - 2, index - "=function(a){a=a.split(\"\");".length());
                logger.debug("Function name: " + r);
                logger.debug("Generating function...");
                final String function = r + "=function(a){a=a.split(\"\");" + subStringToValue + ";return a.join(\"\")" + "};";
                logger.debug("Function to get signature: " + function);
                logger.debug("Getting required sub class...");
                final String subClassName = subStringToValue.substring(0, 2);
                logger.debug("Required Subclass: " +  subClassName);
                // subclass pattern = var XX={...}};g.IK.prototype[...]
                final String subClass0 = StringsUtil.substringBetween(javascriptFile, "var " + subClassName + "={", "};g.");
                logger.debug("Generating subclass...");
                final String subClass = "var " + subClassName + "={" + subClass0 + "};";
                logger.debug("Creating decryption script...");
                final String ds = subClass + function;
                if (ds.length() > 1024) {
                	logger.error("Couldn't get decryption function...");
                	return false;
                }
                logger.debug(ds);
                */
                //logger.debug("Passing function to JS...");
                //decipherViaWebView(encSignatures);
                //String coreFunctions = "";
                logger.debug("Decrypting signatures...");
                String keys0 = "";
                for (final Entry<Integer, String> keys : encSignatures.entrySet()) {
                	keys0 += sigDec.decrypt(keys.getValue()) + "\n";
                }
                if (keys0.equals("\n") || keys0.length() == 0) {
                	logger.error("Failed to extract signatures!");
                	return false;
                }
                //final String result = YTSig.decrypt(keys0);
                decipheredSignature = keys0;
            	logger.debug("Decrypted.");
                //coreFunctions += function;
                
                return true;
            }
        } else if (sigDec != null) {
        	logger.debug("Decrypting signatures...");
            String keys0 = "";
            for (final Entry<Integer, String> keys : encSignatures.entrySet()) {
            	keys0 += sigDec.decrypt(keys.getValue()) + "\n";
            }
            if (keys0.equals("\n") || keys0.length() == 0) {
            	logger.error("Failed to extract signatures!");
            	return false;
            }
            //final String result = YTSig.decrypt(keys0);
            decipheredSignature = keys0;
        	logger.debug("Decrypted.");

        } else {
            decipherViaWebView(encSignatures);
        }
        return true;
    }
	
	private int retryLat = 300;

    private void parseDashManifest(String dashMpdUrl, HashMap<Integer, YtFile> ytFiles, final Proxy proxy) throws IOException {
        Pattern patBaseUrl = Pattern.compile("<BaseURL yt:contentLength=\"[0-9]+?\">(.+?)</BaseURL>");
        String dashManifest;
        BufferedReader reader = null;
        URL getUrl = new URL(dashMpdUrl);
        HttpURLConnection urlConnection = (HttpURLConnection) getUrl.openConnection(proxy);
        urlConnection.setRequestProperty("User-Agent", USER_AGENT);
        try {
            reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
            reader.readLine();
            dashManifest = reader.readLine();

        } finally {
            if (reader != null)
                reader.close();
            urlConnection.disconnect();
        }
        if (dashManifest == null)
            return;
        Matcher mat = patBaseUrl.matcher(dashManifest);
        while (mat.find()) {
            int itag;
            String url = mat.group(1);
            Matcher mat2 = patItag.matcher(url);
            if (mat2.find()) {
                itag = Integer.parseInt(mat2.group(1));
                if (FORMAT_MAP.get(itag) == null)
                    continue;
                if (!includeWebM && FORMAT_MAP.get(itag).getExt().equals("webm"))
                    continue;
            } else {
                continue;
            }
            url = url.replace("&amp;", "&").replace(",", "%2C").
                    replace("mime=audio/", "mime=audio%2F").
                    replace("mime=video/", "mime=video%2F");
            YtFile yf = new YtFile(FORMAT_MAP.get(itag), url);
            //if (URIUtil.hasContent(URIUtil.safeCreate(yf.getUrl()))) {
            	ytFiles.put(itag, yf);
           // }
        }

    }
    
    private static enum ExtractType {
    	VIDEO, AUDIO;
    }
    
    public static final Pattern REGEX_ITAG_IN_URL = Pattern.compile("itag=([0-9]*)");
    
    public static final String[] REQUIRED_ARGS_FOR_VIDEOS = {
    		"expire", "ei", "itag", "keepalive", "requiressl", /* "signature" ,*/ "initcwndbps", "source",
    		"dur", "clen", "otfp", "ipbits", "c", "mime", "gir", "mm", "mn", "ip", "key", "projection_type",
    		
    };
    
    public static final String[] OPTIONAL_ARGS_FOR_VIDEOS = {
    		"fvip", "pl", "ms", "sparams", "mt"
    };

    /**
     * Returns null if no patched used. Else returns hls link.
     * @param getVideoInfo
     * @param localWatchHack
     * @return
     * @throws UnsupportedEncodingException
     */
    private LinkedList<DataSet<Integer, String>> parseVideoMeta(final String getVideoInfo, String pageInfo, 
    		final boolean localWatchHack, final Proxy proxy, final int index, 
    		final boolean verify_controversy, final YTSignatureDecrypter sigDec,
    		final YTSigSoftwareDecoder softSigDec) throws UnsupportedEncodingException {
    	if (pageInfo.contains("verify_controversy")) {
    		logger.debug("Video is limited, attempting skip...");
    		try {
    			final String newPageInfo = URIUtil.downloadPage(URIUtil.safeCreate(
    					"https://www.youtube.com/watch?v=" + videoID + "&bpctr=" + System.currentTimeMillis()  + (list != null ? "&list=" + this.list : "")), //"https://www.youtube.com/verify_controversy?next_url=/" + (URIUtil.safeEncode("watch?v=" + videoID))), 
        				false, proxy);
    			pageInfo = newPageInfo;
    			logger.info("New page info downloaded, attempting to continue.");
    		}catch (Exception e) {
    			logger.error("Couldn't bypass limited video.", e);
    		}
    	}
    	final String raw_json0 = "{\"" + 
    			StringsUtil.substringBetween(pageInfo, "var ytplayer = ytplayer || {};ytplayer.config = ", ";ytplayer.load = function() {");
    	final String raw_json = raw_json0.substring(2, raw_json0.length());
    			//StringsUtil.substringBetween(pageInfo, "var ytplayer = ytplayer || {};ytplayer.config = {\"", "};ytplayer.load = function() {") + "}";
    	final JsonObject parsedJson = (raw_json.startsWith("<!DOCTYPE html>") ? Json.object() : Json.parseDEC(raw_json).asObject());
    	final JsonObject args = parsedJson.getSafe("args").asObject();
    	final String player_resp0 = args.getString("player_response", "{}");
    	//System.out.println(player_resp0);
    	final JsonObject player_resp = Json.parse(player_resp0).asObject();
    	String captions_url = null;
    	//player_resp.printlnf();
    	final JsonArray newCaptions = Json.array();
    	if (player_resp.hasValue("captions")) {
    		final JsonObject captions0 = player_resp.get("captions").asObject();
    		final JsonValue captionsT0 = captions0.get("playerCaptionsTracklistRenderer").asObject();
    		if (captionsT0 != null) {
    			if (captionsT0.asObject().hasValue("captionTracks")) {
    				final JsonArray captionsT = captionsT0.asObject().get("captionTracks").asArray();
    				//captionsT.printlnf();
    				
    				/*
    				if (this.logger.debugEnabled) {
    					this.logger.debug("Printing supported subtitles.");
    					for (final JsonValue caption0 : captionsT) {
                			final JsonObject captions = caption0.asObject();
                    		final String url = captions.getString("baseUrl").replaceAll("\\u0026", "&");
                    		final String captionName = captions.get("name").asObject().getString("simpleText", "English (auto-generated)");
                    		this.logger.debug("Subtitle: " + captionName);
    					}
    				}*/
    				//Deprecated.
            		/*for (final JsonValue caption0 : captionsT) {
            			final JsonObject captions = caption0.asObject();
                		final String url = captions.getString("baseUrl").replaceAll("\\u0026", "&");
                		final String captionName = captions.get("name").asObject().getString("simpleText", "English (auto-generated)");
                		if (!captionName.contains("(auto-generated)") && !captionName.contains("(") && (captionName.contains("English") || captions.getString("languageCode", "00000").equals("en"))) {
                			captions_url = url;
                			break;
                		}
            		}*/
            		for (final JsonValue caption0 : captionsT) {
            			try {
            				final JsonObject captions = caption0.asObject();
                    		final String url = captions.getString("baseUrl").replaceAll("\\u0026", "&");
                    		final String captionName = captions.get("name").asObject().getString("simpleText", "English (auto-generated)");
                    		final String cc = captions.getString("languageCode", "unknown");
                    		final JsonObject toAdd = Json.object();
                    		toAdd.add("cn", captionName);
                    		toAdd.add("cc", cc);
                    		toAdd.add("legacyURL", url);
                    		final String enc = new String(URIUtil.downloadPageFB(URIUtil.safeCreate(url), true, Proxy.NO_PROXY,  URIUtil2.DEFAULT_USER_AGENT));
                    		toAdd.add("webVTT", Base64.encodeBase64S(
                    				WebVTTTransform.transformFromYouTube(
                    						enc)));
                    		newCaptions.add(toAdd);
            			}catch (Exception e) {
            				continue;
            			}
            		}
    			}
    		}
    		//if (this.logger.debugEnabled) {
    		//	this.logger.debug("Printing supported subtitles.");
    		//	ArrayUtils.printLln(newCaptions.values());
    		//}
    	}
		YTComments.push(videoID, new CommentMeta(pageInfo));
    	final boolean liveStream = args.getInt("livestream", 0) == 1;
    	boolean fastMode = this.fastMode;
    	
    	String playlistNextURL = null;
    	//System.out.println("hasPlaylist=" + pageInfo.contains("<div class=\"yt-uix-scroller playlist-videos-list\">") + ",index=" + index);
    	if (pageInfo.contains("<div class=\"yt-uix-scroller playlist-videos-list\">") && index != -1) {
    		final Document doc = Jsoup.parse(pageInfo);
    		final Elements playlistValues = doc.getElementsByClass(" spf-link  playlist-video clearfix  yt-uix-sessionlink      spf-link "); //new Elements();
    		//System.out.println("playList.elements=" + playlistValues.size());
//    		for (final Element e : doc.getElementsByTag("a")) {
//    			final String attr0 = e.attr("href");
//    			final String attr = attr0 != null ? attr0 : "";
//    			final boolean fv = attr.startsWith("/watch") && attr.contains("index=") && attr.contains("list=");
//    			if (fv) {
//    				System.out.println("found playlist entry!");
//    				playlistValues.add(e);
//    			}else {
//    				System.out.println(attr);
//    			}
//    		}
    		if (index < playlistValues.size()) {
    			final Element nextVideo = playlistValues.get(index);
        		final String nextURL = nextVideo.attr("href");
        		//System.out.println(nextURL);
        		playlistNextURL = nextURL;
    		}
    	}
    	
    	boolean isLiveStream = false;
    	Matcher mat;
    	mat = patHlsvp.matcher(getVideoInfo);
        if (mat.find()) {
           isLiveStream = true;
        }
         
        if (!isLiveStream) {
        	// URIUtil.downloadPageS(URIUtil.safeCreate("https://www.youtube.com/watch?v=" + videoID)) - Removed: Less bandwidth used
        	isLiveStream = pageInfo.contains("<meta itemprop=\"isLiveBroadcast\" content=\"True\">");
        }
        //final boolean originalLiveStream = isLiveStream;
        
        if (isLiveStream && (pageInfo.contains("itemprop=\"endDate\"")
        		&& pageInfo.contains("http://schema.org/BroadcastEvent")
        		&& !pageInfo.contains("<meta itemprop=\"endDate\" content=\"2099-12-30T23:00:00+00:00\">") /* NOTE to future generations: Once we reach around 2070, you should UPDATE this. Until then, DO NOT TOUCH THE STRING. */)) {
        	logger.debug("Video WAS a live stream and has premiered already.");
        	isLiveStream = false;
        }else if (isLiveStream) {
        	logger.debug("Video is a live stream.");
        }else {
        	logger.debug("Video is NOT a live stream or has NOT premiered already.");
        }
        
        logger.debug("localWatchHack: " + localWatchHack);
        logger.debug("isLiveStream: " + isLiveStream);
        if (/*!localWatchHack || */!isLiveStream) {
            String title = null, author = null, channelId = null;
            long viewCount = 0, length = 0;
            double avgRating = 0.0;
            String[] tags = new String[0];
            mat = patTitle.matcher(getVideoInfo);
            if (mat.find()) {
                title = URLDecoder.decode(mat.group(1), "UTF-8");
            }

           // mat = patHlsvp.matcher(getVideoInfo);
           // if(mat.find())
           //     isLiveStream = true;
            
            //if (!isLiveStream) {
            	// URIUtil.downloadPageS(URIUtil.safeCreate("https://www.youtube.com/watch?v=" + videoID)) - Removed: Less bandwidth used
            //	isLiveStream = pageInfo.contains("<meta itemprop=\"isLiveBroadcast\" content=\"True\">");
            //}
            	
            mat = patAuthor.matcher(getVideoInfo);
            if (mat.find()) {
                try {
                	author = URLDecoder.decode(mat.group(1), "UTF-8");
                }catch (Exception e) {
                	author = mat.group(1);
                }
            }
            mat = patChannelId.matcher(getVideoInfo);
            if (mat.find()) {
                channelId = mat.group(1);
            }
            mat = patLength.matcher(getVideoInfo);
            if (mat.find()) {
                length = Long.parseLong(mat.group(1));
            }
            mat = patViewCount.matcher(getVideoInfo);
            if (mat.find()) {
                viewCount = Long.parseLong(mat.group(1));
            }else if (getVideoInfo.contains("viewCount%22%3A%22")) {
            	viewCount = MathUtil.safeParseL(
            			StringsUtil.substringBetweenO(getVideoInfo, "viewCount%22%3A%22", "%22"), -1);
            }
            mat = patAvgRating.matcher(getVideoInfo);
            if (mat.find()) {
            	try {
            		avgRating = Double.parseDouble(mat.group(1).replace('&', '\u0000'));
            	}catch (Exception e) {
    				avgRating = -1D;
    			}
            }
            mat = patTags.matcher(getVideoInfo);
            if (mat.find()) {
            	final String g0 = mat.group();
            	final Charset chars = CharsetUtil.charset(g0);
            	try {
            		tags = URLDecoder.decode(g0, chars.name()).replaceAll(Pattern.quote("+"), " ").split(",");
            	}catch (Exception e) {
            		tags = new String[0];
            	}
            }else if (getVideoInfo.contains("%22keywords%22%3A%5B")) {
            	String g0 = StringsUtil.substringBetweenO(getVideoInfo, "%22keywords%22%3A%5B", "%5D%2C%22");
            	if (g0 == null) {
            		tags = new String[0];
            	}else {
            		g0 = URIUtil.safeDecode(g0);
            		final String[] splits = g0.split(",");
            		if (splits == null || splits.length == 0) {
            			tags = new String[0];
            		}else {
            			String[] temp = new String[splits.length];
            			int i = 0;
            			for (String tag : splits) {
            				if (tag.startsWith("\"")) {
            					tag = tag.substring(1);
            				}
            				if (tag.endsWith("\"")) {
            					tag = tag.substring(0, tag.length() - 1);
            				}
            				temp[i] = tag.replaceAll(Pattern.quote("\\u0026"), "&");
            				i += 1;
            			}
            			tags = temp;
            		}
            	}
            }
            boolean cannotExtract = false;
            if (title == null && pageInfo.contains("<meta itemprop=\"regionsAllowed\" content=\"\">")) {
            	cannotExtract = true;
            	title = "ERROR: This content is geographically censored.";
            }
            videoMeta = new VideoMeta(videoID, title, author, channelId, length, viewCount, isLiveStream, avgRating,
            		MetadataExtractor.parseEndVideoData(videoID, author), tags, MetadataExtractor.getEndScreen(videoID, author), 
            		getVideoInfo.contains("Playback on other websites has been disabled by the video owner."), captions_url,
            		newCaptions);
            videoMeta.setControverisal(verify_controversy);
            videoMeta.set360(pageInfo.contains("//support.google.com/youtube/answer/6178631"));
            if (playlistNextURL != null) {
            	videoMeta.setPlaylistNextURL(playlistNextURL);
            }
            videoMeta.__applyMaps(pageInfo, getVideoInfo);
            if (cannotExtract) {
            	return null;
            }
            String fastVideo = null;
            try {
            	fastVideo = URIUtil.safeDecode(args.getString("url_encoded_fmt_stream_map"))
         		.replaceAll(Pattern.quote("\\u0026"), "&");
            }catch (Exception e) {}
            String fastVideoAltSources = null;
            try {
            	fastVideoAltSources = URIUtil.safeDecode(args.getString("adaptive_fmts"))
        		.replaceAll(Pattern.quote("\\u0026"), "&");
            	//JVMUtil.async_println(fastVideoAltSources);
            }catch (Exception e) {}
        	if (!isLiveStream) {
        		if (!fastMode) {
        			return null;
        		}
        		final LinkedList<DataSet<Integer, String>> outputSources = new LinkedList<DataSet<Integer, String>>();
        		if (localWatchHack && (fastVideo == null || fastVideoAltSources == null)
        				&& args.contains("hlsvp")) {
        			logger.debug("[Fast Mode 2] This was a stream... But, it's still on m3u8. Uhh....");
        			outputSources.add(new DataSet<Integer, String>(1, args.getString("hlsvp")));
        			return outputSources;
        		}
        		if ((!pageInfo.contains("og:restrictions:age") && fastVideoAltSources != null
        				) || (fastVideoAltSources != null && !JVMUtil.isArg("--forceVideoInfoExtraction"))) {
        			logger.debug("[Fast Mode 2] Extractor used: Youtube Direct");
        			String comb = (fastVideo + "," + fastVideoAltSources);
            		final String[] sources = comb.split("(,(?=\\S))"); // was ','
            		final boolean signatureDecryptionRequired = comb.contains("?s=") || comb.contains("&s=")
            				|| comb.contains("signature%3D");
            		int fuckUps = 0;
            		/**
            		 * Some sources accidentally split in the middle of essential data, this is a hack to fix that.
            		 */
            		final LinkedList<String> unclaimedSigs = new LinkedList<String>();
            		if (logger.traceEnabled) {
            			logger.trace("source list: " + comb);
            		}
            		for (String source : sources) {
            			if ((source.contains("&url=") || source.startsWith("url="))
            					&& !unclaimedSigs.isEmpty() && (!source.contains("s=")
            							&& !source.contains("sig=") && !source.contains("lsig="))) {
            				logger.warn("[Fast Mode 2] This source seems to lack a signature, claiming one from the unused list...");
            				source += "&" + unclaimedSigs.removeFirst();
            			}
            			/*
            			 * WARNING: Note of self joy ahead.
            			 * 
            			 * FUCK YES
            			 * IT FUCKING FINALLY WORKS
            			 * 
            			 * FUCK YOU JEWTUBE FOR YOUR CLOSED HORSE SHIT
            			 * 
            			 * ALSO THE FUCKING TIME REDUCTION, LITERALLY FROM >10s to ~5s, WHAT THE FUCK
            			 * 
            			 * (Also for those having to use nPlayer fallback, good luck *smile w/ wink*)
            			 */
            			final Map<String, String> sourceList = URIUtil.queryStringToMap_NoDecode(source);
            			if (JVMUtil.isArg("--FastMode.debug")) {
            				ArrayUtils.printLln(sourceList);
                			JVMUtil.async_println("=======================");
            			}
            			if (sourceList.containsKey("source") && (sourceList.get("source").equals("yt_otf")
            					&& sourceList.containsKey("dur") && sourceList.get("dur").equals("0.000"))) {
            				logger.error("[Fast Mode 2] This is an invalid source... [source=" + sourceList.get("source") + "]");
            				fuckUps += 1;
            				continue;
            			}
            			// arr.first("url") << each::(arr.item());
            			//String processedURL = sourceList.get("url");
            			String url = sourceList.get("url");
            			if (url == null) {
            				logger.error("[Fast Mode 2] the url was null....");
            				if (logger.debugEnabled) {
            					logger.debug("source: " + source);
            				}
            				//JVMUtil.async_println("Original: " + source);
            				//ArrayUtils.printLln(sourceList);
            				if (source.contains("&sig=") || source.contains("&s=")
            						|| source.startsWith("s=") || source.startsWith("sig=")) {
            					unclaimedSigs.add(source);
            				}else {
            					fuckUps += 1;
            				}
            				//fuckUps += 1;
            				continue;
            			}
            			String sig = null;
            			String sigDataName = sourceList.get("sp");
            			if (sigDataName == null) {
            				sigDataName = "";
            			}
            			if (!sourceList.containsKey("s") && !sourceList.containsKey("signature")
            					&& !sourceList.containsKey("lsig")) {
            				logger.error("[Fast Mode 2] Uhh, this URL has no signature information, skipped.");
            				if (logger.debugEnabled) {
            					logger.debug("source: " + source);
            				}
            				//JVMUtil.async_println("Original: " + source);
            				//ArrayUtils.printLln(sourceList);
            				fuckUps += 1;
            				continue;
            			}else if ((!sourceList.containsKey("s") /* && !(sourceList.get("s").contains(".") ) */)
            					/* || */&& (!sourceList.containsKey("signature") /* && !sourceList.get("signature").contains(".") */)
            					/* || */&& (!sourceList.containsKey("lsig") || sourceList.get("lsig").contains("PROBABLY WON'T EXIST, ALSO INVALID URL ANYWAY"))) {
            				logger.error("[Fast Mode 2] Uhh, this signature is broken, panicing...");
            				//JVMUtil.async_println("Original: " + source);
            				//ArrayUtils.printLln(sourceList);
            				fuckUps += 5;
            				continue;
            			}else if (signatureDecryptionRequired && sourceList.containsKey("s")) {
            				//sig = (JVMUtil.isArg("--Signatures.useLegacyJS") ? 
            				//		sigDec.decrypt(sourceList.get("s")) : softSigDec.decrypt(sourceList.get("s")));
            				if (!JVMUtil.isArg("--Signatures.useSoftwareJS") && sigDec != null) {
            					sig = sigDec.decrypt(sourceList.get("s"));
            				}else {
            					sig = softSigDec.decrypt(sourceList.get("s"));
            				}
            				//sig = softSigDec.decrypt(sourceList.get("s"));
            			}else if (sourceList.containsKey("lsig")) {
            				sig = sourceList.get("lsig");
            			}else {
            				sig = sourceList.get("signature");
            			}
            			if (sig == null) {
            				logger.error("[Fast Mode 2] Uhh, this URL gave no signature information, skipped.");
            				if (logger.traceEnabled) {
            					logger.error("Since you have trace enabled, printing the source list...");
            					ArrayUtils.printLln(sourceList);
            				}
            				fuckUps += 1;
            				continue;
            			}
            			if (sourceList.containsKey("lsig") && sigDataName.equals("sig")) {
            				if (sigDataName.isEmpty()) {
            					sigDataName = "sig";
            				}
            				url += "?" + sigDataName + "=" + sigDec.decrypt(sig);
            			}else if (sourceList.containsKey("lsig")) {
            				if (sigDataName.isEmpty()) {
            					sigDataName = "lsig";
            				}
            				url += "?" + sigDataName + "=" + sig;
            			}else {
            				if (sigDataName.isEmpty()) {
            					sigDataName = "signature";
            				}
            				url += "?" + sigDataName + "=" + sig;
            			}
            			
            			
            			
            			for (final Map.Entry<String, String> entry : sourceList.entrySet()) {
            				if (entry.getKey().equals("s") || entry.getKey().equals("signature")
            						|| entry.getKey().equals("lsig")
            						|| entry.getKey().equals("type") || entry.getKey().equals("xtags")
            						|| entry.getKey().equals("url") || entry.getKey().equals("quality")
            						|| entry.getKey().equals("size") || entry.getKey().equals("index")
            						|| entry.getKey().equals("quality_label")
            						|| entry.getKey().equals("fps") || entry.getKey().equals("projection_type")
            						|| entry.getKey().equals("primaries")
            						|| entry.getKey().equals("init")
            						|| entry.getKey().equals("eotf") || entry.getKey().equals("sp")
            						|| entry.getKey().equals("bitrate") || entry.getKey().equals("stream_type")
            						|| entry.getKey().equals(" vorbis\"") || entry.getKey().equals("beids")
            						|| entry.getKey().equals("txp")
            						) {
            					continue;
            				}
            				url += ("&" + entry.getKey() + "=" + entry.getValue());
            			}
            			//final String requiredTags = sourceList.get("sparams");
            			//final String[] rqtd = URIUtil.safeDecode(requiredTags).split(",");
            			
            			//url += "&sparams=" + requiredTags;
            			
            			//for (final String requiredTag : rqtd) {
            			//	url += "&" + requiredTag + "=" + sourceList.get(requiredTag);
            			//}
            			
            			if (JVMUtil.isArg("--FastMode.debug")) {
            				JVMUtil.async_println("Results:");
                			JVMUtil.async_println(url);
            			}
            			//JVMUtil.async_println(URIUtil.getResponseCode(URIUtil.safeCreate(url)));
            			
            			final int itag = MathUtil.safeParseI(sourceList.get("itag"), -1);
            			outputSources.add(
            							new DataSet<Integer, String>(itag, url));
            		}
            		if (outputSources.isEmpty() && logger.debugEnabled) {
    					logger.error("[Fast Mode 2] Extraction failed.... Uhh...since you have debug mode on, dumping everything I know.");
    					ArrayUtils.printLln(sources);
    				}
        		}else {
        			logger.warn("[Fast Mode 2] Age restricted video found, falling back...");
        			logger.debug("[Fast Mode 2] Extractor used: Youtube Embed");
        			final Map<String, String> sourceLists = URIUtil.queryStringToMap_NoDecode(getVideoInfo);
        			final boolean signatureDecryptionRequired = getVideoInfo.contains("?s=") 
        					|| getVideoInfo.contains("&s=") || getVideoInfo.contains("signature%3D");
            		int fuckUps = 0;
        			if (sourceLists.containsKey("url_encoded_fmt_stream_map")) {
        				final String[] links = URIUtil.safeDecode(
        						sourceLists.get("url_encoded_fmt_stream_map") + "," + 
        								sourceLists.get("adaptive_fmts")).split("(,(?=\\S))"); //was Pattern.quote(",")
        				
        				for (final String link : links) {
        					final Map<String, String> sourceList = URIUtil.queryStringToMap_NoDecode(
        							URIUtil.safeDecode(link));
        					if (JVMUtil.isArg("--FastMode.debug")) {
                				ArrayUtils.printLln(sourceList);
                    			JVMUtil.async_println("=======================");
                			}
                			// arr.first("url") << each::(arr.item());
                			//String processedURL = sourceList.get("url");
                			String url = sourceList.get("url");
                			if (url == null) {
                				logger.error("[Fast Mode 2] the url was null....");
                				//JVMUtil.async_println("Original: " + link);
                				//ArrayUtils.printLln(sourceList);
                				fuckUps += 1;
                				continue;
                			}
                			String sig = null;
                			if (!sourceList.containsKey("s") && !sourceList.containsKey("signature")) {
                				logger.error("[Fast Mode 2] Uhh, this URL has no signature information, skipped.");
                				fuckUps += 1;
                				continue;
                			}else if (signatureDecryptionRequired && sourceList.containsKey("s")) {
                				if (sourceList.get("s") == null) {
                					logger.error("[Fast Mode 2] Uhh, this signature is null... skipped.");
                					//JVMUtil.async_println("Original: " + link);
                					//ArrayUtils.printLln(sourceList);
                    				fuckUps += 1;
                    				continue;
                				}
                				//sig = (JVMUtil.isArg("--Signatures.useLegacyJS") ? 
                				//		sigDec.decrypt(sourceList.get("s")) : softSigDec.decrypt(sourceList.get("s")));
                				if (!JVMUtil.isArg("--Signatures.useSoftwareJS") && sigDec != null) {
                					sig = sigDec.decrypt(sourceList.get("s"));
                				}else {
                					sig = softSigDec.decrypt(sourceList.get("s"));
                				}
                				//sig = softSigDec.decrypt(sourceList.get("s"));
                			}else {
                				sig = sourceList.get("signature");
                			}
                			if (sig == null) {
                				logger.error("[Fast Mode 2] Uhh, this URL gave no signature information, skipped.");
                				fuckUps += 1;
                				continue;
                			}
                			url += "?signature=" + sig;
                			
                			
                			for (final Map.Entry<String, String> entry : sourceList.entrySet()) {
                				if (entry.getKey().equals("s") || entry.getKey().equals("signature")
                						|| entry.getKey().equals("type") || entry.getKey().equals("xtags")
                						|| entry.getKey().equals("url") || entry.getKey().equals("quality")
                						|| entry.getKey().equals("size") || entry.getKey().equals("index")
                						|| entry.getKey().equals("quality_label")
                						|| entry.getKey().equals("fps") || entry.getKey().equals("projection_type")
                						|| entry.getKey().equals("primaries")
                						|| entry.getKey().equals("init")
                						|| entry.getKey().equals("eotf") || entry.getKey().equals("sp")
                						|| entry.getKey().equals("bitrate") || entry.getKey().equals(" vorbis\"")
                						|| entry.getKey().equals("beids") || entry.getKey().equals("txp")) {
                					continue;
                				}
                				url += ("&" + entry.getKey() + "=" + entry.getValue());
                			}
                			
                			final int itag = MathUtil.safeParseI(sourceList.get("itag"), -1);
                			outputSources.add(
                							new DataSet<Integer, String>(itag, url));
        				}
        				if (outputSources.isEmpty() && logger.debugEnabled) {
        					logger.error("[Fast Mode 2] Extraction failed.... Uhh...since you have debug mode on, dumping everything I know.");
        					ArrayUtils.printLln(links);
        				}
        			}else {
        				logger.error("[Fast Mode 2] sourceList didn't contain any video information...");
        			}
        		}
//        		JVMUtil.async_println(fastVideoAltSources);
//        		JVMUtil.async_println(fastVideo);
//        		/*
//        		String[] sources = (fastVideo.contains("&signature=")
//        				|| fastVideoAltSources.contains("&signature=")
//        				? (fastVideoAltSources + ";" +  fastVideo).split(
//                				Pattern.quote(";")) :
//                					 (fastVideoAltSources + ";" +  fastVideo).split(
//                                    				Pattern.quote("sp=signature")));*/
//        		
//        		final String comb = fastVideoAltSources + ";" + fastVideo;
//        		final String[] sources = comb.split("; ");
//        		final boolean signatureDecryptionRequired = comb.contains("?s=") || comb.contains("&s=");
//        		final LinkedList<DataSet<Integer, String>> outputSources = new LinkedList<DataSet<Integer, String>>();
//        		
//        		for (final String source : sources) {
//        			String url = StringsUtil.substringBetweenO(source, "&url=", ";");
//        			if (url == null && !source.contains(",")) {
//        				url = source.substring(source.indexOf("&url=") + "&url=".length());
//        			}
//        			if (url.contains(",")) {
//        				url = url.substring(0, url.indexOf(','));
//        			}
//        			JVMUtil.async_error("CORE URL: ");
//        			JVMUtil.async_error(url);
//        		}
        		
        		//final int itag = MathUtil.safeParseI(
				//		StringsUtil.substringBetweenF(url, "&itag=", "&", "lol"), -1);
    			//outputSources.add(
				//		new DataSet<Integer, String>(itag, url));
        		
        		
//        		if (JVMUtil.isArg("--FastMode.autoRetryExtracting") && outputSources.isEmpty()) {
//        			try {
//        				Thread.sleep(retryLat);
//        				this.retryLat += MathUtil.randInt(400, 600);
//        			}catch (Exception e) {}
//        			logger.info("Retrying...");
//        			String newPageInfo;
//					try {
//						newPageInfo = URIUtil.downloadPage(URIUtil.safeCreate(
//								"https://www.youtube.com/watch?v=" + videoID + "&bpctr=" + System.currentTimeMillis()  + (list != null ? "&list=" + this.list : "")), //"https://www.youtube.com/verify_controversy?next_url=/" + (URIUtil.safeEncode("watch?v=" + videoID))), 
//								false, proxy);
//					} catch (IOException e) {
//						throw new RuntimeException(e);
//					}
//        			pageInfo = newPageInfo;
//        			return this.parseVideoMeta(getVideoInfo, pageInfo, localWatchHack, proxy, index, 
//        					verify_controversy, sigDec);
//        		}
        		if (outputSources.isEmpty()) {
        			if (outputSources.size() <= 3) {
        				return null;
        			}
        			if (JVMUtil.isArg("--onlyFastMode")) {
        				return outputSources;
        			}
        			return null;
        		}else {
        			return outputSources;
        		}
        	}
        	/**
        	if (fastVideo != null) {
        		//Also, nice idea dingus, you should be decode these 'signatures'
        		//Finally, fastVideo contains audio+video streams. (up to 720p)string
    			final LinkedList<String> sources_video = new LinkedList<String>();
    			final LinkedList<String> sources_audio = new LinkedList<String>();
        		String[] fastVideoSplit = ArrayUtils.from(fastVideo.split(Pattern.quote("&url=")), 1);
        		
        		if (fastVideoSplit != null) {
        			for (final String fVS : fastVideoSplit) {
            			final String[] values = fVS.split(Pattern.quote("; codecs=\""));
            			final String url = values[0];
            			if (URIUtil.hasContent(URIUtil.safeCreate(url))) {
            				if (url.contains("video/webm") || url.contains("video/mp4")) {
            					sources_video.add(url);
            				}else if (url.contains("audio/webm") || url.contains("audio/mp4")) {
            					sources_audio.add(url);
            				}
            			}else {
            			}
            		}
        		}
        		logger.debug("YouTubeExtractor.FastMode.audioSources: " + sources_audio.size());
        		logger.debug("YouTubeExtractor.FastMode.videoSources: " + sources_video.size());
        		if (!sources_video.isEmpty()) {
        			// can use fastMode, use it.
        			fastMode = true;
        			final LinkedList<DataSet<Integer, String>> outputs = new LinkedList<DataSet<Integer, String>>();
        			for (final String videoSource : sources_video) {
        				final Matcher m = REGEX_ITAG_IN_URL.matcher(videoSource);
        				int itag = -1;
        				if (m.find()) {
        					itag = MathUtil.safeParseI(m.group(0).split(Pattern.quote("="))[1], -1);
        				}
        				outputs.add(new DataSet<Integer, String>(itag, videoSource));
        			}
        			for (final String audioSource : sources_audio) {
        				final Matcher m = REGEX_ITAG_IN_URL.matcher(audioSource);
        				int itag = -1;
        				if (m.find()) {
        					itag = MathUtil.safeParseI(m.group(0).split(Pattern.quote("="))[1], -1);
        				}
        				outputs.add(new DataSet<Integer, String>(itag, audioSource));
        			}
        			if (sources_audio.isEmpty() || sources_video.isEmpty()) {
        				// double check
        				return null;
        			}else {
        				return outputs;
        			}
        		}else {
        			fastMode = false;
        			return null;
        		}
        	}**/
        }else {
        	final String[] tags = args.getString("keywords", "").split(",");
        	final double rating = MathUtil.safeParseD(args.getString("avg_rating", "-1"), -1D);
        	final String author = args.getString("author", "N/A");
        	final String title = args.getString("title", "N/A");
        	String hlsVp = args.getString("hlsvp", null);
        	if (hlsVp == null) {
        		final JsonObject player = Json.parseDEC(args.get("player_response").asString()).asObject();
        		hlsVp = player.get("streamingData").asObject().getString("hlsManifestUrl");
        	}
        	final String channelID = args.getString("ucid", "N/A");
        	final int viewCount = args.getInt("view_count", -1);
        	videoMeta = new VideoMeta(videoID, title, author, 
        			channelID, 0, viewCount, liveStream, rating, MetadataExtractor.parseEndVideoData(videoID, author), tags,
        			MetadataExtractor.getEndScreen(videoID, author), false, captions_url,
        			newCaptions);
        	videoMeta.setControverisal(verify_controversy);
        	if (hlsVp == null) {
        		return null;
        	}else {
        		return new LinkedList<DataSet<Integer, String>>(
            			Collections.singleton(new DataSet<Integer, String>(1, hlsVp)));
        	}
        }
		return null;  
    }

    private void readDecipherFunctFromCache() {
        File cacheFile = new File(FileUtil.tempDir() + "/" + CACHE_FILE_NAME);
        // The cached functions are valid for 2 weeks
        if (cacheFile.exists() && (System.currentTimeMillis() - cacheFile.lastModified()) < 1209600000) {
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(new FileInputStream(cacheFile), "UTF-8"));
                decipherJsFileName = reader.readLine();
                decipherFunctionName = reader.readLine();
                decipherFunctions = reader.readLine();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * Parse the dash manifest for different dash streams and high quality audio. Default: false
     */
    public void setParseDashManifest(boolean parseDashManifest) {
        this.parseDashManifest = parseDashManifest;
    }


    /**
     * Include the webm format files into the result. Default: true
     */
    public void setIncludeWebM(boolean includeWebM) {
        this.includeWebM = includeWebM;
    }


    /**
     * Set default protocol of the returned urls to HTTP instead of HTTPS.
     * HTTP may be blocked in some regions so HTTPS is the default value.
     * <p/>
     * Note: Enciphered videos require HTTPS so they are not affected by
     * this.
     */
    public void setDefaultHttpProtocol(boolean useHttp) {
        this.useHttp = useHttp;
    }

    private void writeDeciperFunctToChache() {
        File cacheFile = new File(FileUtil.tempDir() + "/" + CACHE_FILE_NAME);
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cacheFile), "UTF-8"));
            writer.write(decipherJsFileName + "\n");
            writer.write(decipherFunctionName + "\n");
            writer.write(decipherFunctions);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void decipherViaWebView(final HashMap<Integer, String> encSignatures) {
        final StringBuilder stb = new StringBuilder(decipherFunctions + " function decipher(");
        stb.append("){return ");
        for (int i = 0; i < encSignatures.size(); i++) {
            int key = ArrayUtils.indexOfA(encSignatures, i); //encSignatures.get(i); //(i);
            if (i < encSignatures.size() - 1)
                stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
                        append("')+\"\\n\"+");
            else
                stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
                        append("')");
        }
        stb.append("};decipher();");

        final ScriptEngineManager engineManager = nScripting.cached(); //new ScriptEngineManager();
        final ScriptEngine engine = engineManager.getEngineByName("nashorn"); // javascript
        if (engine == null) {
        	throw new RuntimeException("YoutubeExtractor.decipher() -> Cannot find JS engine!");
        }
        lock.lock();
        try {
			decipheredSignature = (String) engine.eval(stb.toString());
	        jsExecuting.signal();
	        lock.unlock();
	        logger.trace("Time taken to execute decryption JS: " + (System.currentTimeMillis() - start));
		} catch (ScriptException e) {
			e.printStackTrace();
			lock.unlock();
		}
//        new Handler(Looper.getMainLooper()).post(new Runnable() {
//
//            @Override
//            public void run() {
//                new JsEvaluator(context).evaluate(stb.toString(), new JsCallback() {
//                    @Override
//                    public void onResult(String result) {
//                        lock.lock();
//                        try {
//                            decipheredSignature = result;
//                            jsExecuting.signal();
//                        } finally {
//                            lock.unlock();
//                        }
//                    }
//
//                    @Override
//                    public void onError(String errorMessage) {
//                        lock.lock();
//                        try {
//                            if(LOGGING)
//                                Log.e(LOG_TAG, errorMessage);
//                            jsExecuting.signal();
//                        } finally {
//                            lock.unlock();
//                        }
//                    }
//                });
//            }
//        });
    }
    
    private void decipherViaWebView2(final HashMap<Integer, String> encSignatures, final String initalJS) {
        final StringBuilder stb = new StringBuilder(decipherFunctions + " function decipher(");
        stb.append("){return ");
        for (int i = 0; i < encSignatures.size(); i++) {
            int key = ArrayUtils.indexOfA(encSignatures, i); //encSignatures.get(i); //(i);
            if (i < encSignatures.size() - 1)
                stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
                        append("')+\"\\n\"+");
            else
                stb.append(decipherFunctionName).append("('").append(encSignatures.get(key)).
                        append("')");
        }
        stb.append("};decipher();");

        final ScriptEngineManager engineManager = nScripting.cached(); //new ScriptEngineManager();
        final ScriptEngine engine = engineManager.getEngineByName("nashhorn"); // javascript
        if (engine == null) {
        	throw new RuntimeException("YoutubeExtractor.decipher() -> Cannot find JS engine!");
        }
        lock.lock();
        try {
			decipheredSignature = (String) engine.eval(initalJS + stb.toString());
	        jsExecuting.signal();
	        lock.unlock();
	        logger.trace("Time taken to execute decryption JS: " + (System.currentTimeMillis() - start));
		} catch (ScriptException e) {
			e.printStackTrace();
			lock.unlock();
		}
    }
}
